<?php
ArrowController::importModel("org.arrowplatform.orm.ArrowOrmConfig");
ArrowController::importModel("org.arrowplatform.orm.ArrowOrmSqlRouter");

/**
 * @author Pawel Giemza
 * @version 1.0
 * @package Arrow
 * @subpackage Orm
 * @link http://arrowplatform.org/
 * @copyright 2009 3code 
 * @license GNU LGPL
 * 
 * @date 2009-03-06 
*/


/** 
 * Delivers API for selecting rows.
 * 
 * Provides functions which allow to generate conditions used to describe rows 
 * to be requested from DB by Class Persistent.
 */
class ArrowOrmCriteria {

	const C_EQUAL = '==';  		/**<Equality condition (SQL: = ) */
	const C_NOT_EQUAL  = '!=';	/**<Inequality condition (SQL: != ) */
	const C_GREATER_EQUAL = '>='; /**<Greater or equal condition (SQL: >= )*/
	const C_GREATER_THAN = '>'; 	/**<Greater than condition (SQL: > )*/
	const C_LESS_EQUAL = '<='; 	/**<Less or equal condition (SQL: <= )*/
	const C_LESS_THAN = '<'; 	/**<Greater than condition (SQL: < )*/

	const C_IN = 'IN';      	/**<In operator (SQL: IN) <br /> Note: this operator requires $value to be an array*/
	const C_NOT_IN  = 'NOT IN'; 	/**<Not In operator (SQL: NOT IN)<br /> Note: this operator requires $value to be an array*/
	const C_BETWEEN  = 'BETWEEN'; 	/**<Between operator (SQL: BETWEEN) <br /> Note: this operator requires $value to be 2 element array*/	

	const C_LIKE  = 'LIKE';   		/**<Like operator (SQL: LIKE)*/
	const C_ILIKE = 'ILIKE';   		/**<Case insensitive Like operator (SQL: ILIKE)*/
	const C_NOT_LIKE  = 'NOT LIKE';	/**<Not Like operator (SQL: NOT LIKE)*/
	const C_NOT_ILIKE  = 'NOT ILIKE';	/**<Case insensitive Not Like operator (SQL: NOT ILIKE)*/	
	
	const C_BIT_OR = '|';	/**<Bit OR */
	const C_BIT_AND = '&';	/**<Bit AND */
	const C_BIT_XOR = '^';	/**<Bit XOR */
	
	const C_RELATION  = '===';/**<Relation operator (SQL: =) <br /> Note: When this operator is used $value will not be escaped with " ' " */
	const C_OR_GROUP = 'OR';	/**<Start nested OR group*/
	const C_AND_GROUP = 'AND';	/**<Start nested AND group*/
	
	const C_CUSTOM = 'CUSTOM';	/**Custom condition*/
	
	const START = 'START'; 		/*Start nested group (internal use only)*/
	const END = 'END';			/*End nested group (internal use only) */
	
	const O_DESC = "DESC"; /**<Descending order */
	const O_ASC = "ASC"; 	/**<Ascending order */
	const O_RAND = "RAND"; 	/**<Random order */
	
	const A_COUNT = "COUNT";	/**<Aggregate: count */	
	const A_COUNT_DISTINCT = "COUNT_DISTINCT"; /**  */
	const A_MIN = "MIN";		/**<Aggregate: min */
	const A_MAX = "MAX";		/**<Aggregate: max */
	const A_SUM = "SUM";		/**<Aggregate: sum */
	const A_AVG = "AVG";		/**<Aggregate: avg */
	
	const A_YEAR = "YEAR" ;		/**<Date: Year */
	const A_MONTH = "MONTH" ;		/**<Date: Month */
	const A_DAY = "DAY" ;		/**<Date: Day */


	static private $condition_names =  array(self::C_EQUAL, self::C_NOT_EQUAL, self::C_GREATER_EQUAL, self::C_GREATER_THAN, self::C_LESS_EQUAL, self::C_LESS_THAN, self::C_IN, self::C_NOT_IN, self::C_BETWEEN, self::C_LIKE, self::C_ILIKE, self::C_NOT_LIKE, self::C_NOT_ILIKE, self::C_RELATION, self::C_CUSTOM);
	static private $group_conditions = array(self::C_IN, self::C_NOT_IN, self::C_BETWEEN);
	/**
	 * Array of conditions
	 *
	 * @var Array
	 */
	private $data = array();
			
	/**
	 * List of nested groups (AND/OR) 
	 *
	 * @var Array
	 */
	private $groups = array(self::C_AND_GROUP);

	/**
	 * Information whether this is first or last condition in group
	 * required for correct nesting of conditions
	 *
	 * @var Boolean
	 */	
	protected $firstlast = true;
	
	protected $aggregates = false;

	protected $emptylist = true;	//zmin nazwe bo myli
	
	/**
	 * Constructor.
	 * 
	 * @param $model
	 * @param $data
	 * @return 
	 */
	public function __construct( $model='', $data='')
	{			
    if (!empty($model))
      $this->setModel($model);
    
    if (!empty($data))
      $this->fromString($data);
	}

	/**
	 * Routing function for string representation parser (fromString).
	 * @param $a1
	 * @param $a2
	 * @param $a3
	 * @return unknown_type
	 */
	private function add($a1, $a2='', $a3=''){
    //where conditions
    if ( in_array($a2, self::$condition_names, true) ){
		if (in_array($a2, self::$group_conditions, true)){
			if( is_string($a3) )
        $a3 = explode(',',$a3);
			$this->addCondition($a1,$a3,$a2);
		}else
    		$this->addCondition($a1,$a3,$a2);
    }    
    else
    //order by
    if ( strcasecmp($a1, 'ORDERBY') == 0 ){
      $this->addOrderBy($a2,$a3);
    }	
    else
    //group by
    if ( strcasecmp($a1, 'GROUPBY') == 0 ){
      $this->addGroupBy($a2);
    }	
    else
    //limit
    if ( strcasecmp($a1, 'LIMIT') == 0 ){
      $this->setLimit($a2,$a3);
    }	
    else
    //grouping
    if ( strcasecmp($a1, 'GROUP') == 0 ){
      if (strcasecmp($a2, 'START') == 0 )
            $this->startGroup($a3);
      elseif (strcasecmp($a2, 'END') == 0 )
            $this->endGroup();    
    }	   
    else
    //set (model, distinct, etc)
    if ( strcasecmp($a1, 'SET') == 0 ){
      if (strcasecmp($a2, 'MODEL') == 0 )
        $this->setModel($a3);
      if (strcasecmp($a2, 'DISTINCT') == 0 )
        $this->setDistinct();
      if (strcasecmp($a2, 'EMPTYLIST') == 0 )
        $this->setEmptyList();
    }	         
    else
    //remove columns
    if ( strcasecmp($a1, 'REMOVE') == 0 ){
      $this->removeColumn($a2);
    
    }	         
    else
    //column
    {
      $this->addColumn($a1,$a2,$a3);
    }	
	}

	/**
	 * Add single condition to Criteria object 
	 * 
	 * @param $column
	 * @param $value
	 * @param $condition
	 * @return 
	 */
	public function addCondition( $column, $value, $condition=self::C_EQUAL){
		//ignore empty CUSTOM conditions
		if ($condition == self::C_CUSTOM && empty($value) ){
			return;
		}
		if( $condition == self::END ) {
			$this->firstlast = true;
			$value = NULL ;
			$this->groups = array_slice($this->groups,0,count($this->groups)-1);
		}
		if (count($this->groups) && $this->firstlast==false) 	//add AND/OR between conditions
			$this->data['conditions'][] = array( 'column'=>'', 'value'=>NULL, 'condition'=>end($this->groups)); 
		$this->data['conditions'][] = array( 'column'=>$column, 'value'=>$value, 'condition'=>$condition );
		if( $condition == self::START ) {
			$this->firstlast = true;
			$this->groups[] = $value;
		}
		else $this->firstlast = false;
		//ArrowLogger::log("[ArrowOrmCriteria] $column:$condition:$value",ArrowLogger::EL_INFO);			
	}

	public function addSearchCondition($columns, $value, $condition = self::C_LIKE){
		if (!is_array($columns))$columns = array($columns);
		$this->startGroup(self::C_OR_GROUP);
		foreach ($columns as $column){
			$this->addCondition($column,$value,$condition);
		}
		$this->endGroup();
	}
	
	/**
	 * Custom condition (in pure SQL) 
	 * 
	 * @param string $value - condition
	 * @param array(string) - tables which are in condition
	 * @return 
	 */
	public function addCustomCondition($value, $tables = array() ){
		if (count($this->groups) && $this->firstlast==false) 	//add AND/OR between conditions
			$this->data['conditions'][] = array( 'column'=>'', 'value'=>NULL, 'condition'=>end($this->groups)); 
		$this->data['conditions'][] = array( 'column'=>'', 'value'=>$value, 'condition'=>'CUSTOM', 'tables'=>$tables);
		$this->firstlast = false;
		//ArrowLogger::log("[ArrowOrmCriteria] $column:$condition:$value",ArrowLogger::EL_INFO);			
	}

	/**
	 * Add/chenge name of single column in Criteria object
	 * 
	 * @note By default Criteria includes list of all fields stored in configuration of model with witch it will be used.
	 * Call setEmptyList() in order to override this.
	 * 
	 * @param $column - name of column to add/change
	 * @param $alias - index under which this column will be returned
	 * @param $aggregate - aggergete function to be used with this column use one of A_* constant here
	 * @return 
	 */
	public function addColumn( $column, $alias='', $aggregate=''){
		if (is_array($column)){
			foreach( $column as $record){
				if(!is_array($record))
					$this->addColumn($record);
				else
					$this->addColumn($record['column'], $record['alias'], $record['aggregate']);
			}
		}
		else{
			//ArrowLogger::log("[ArrowOrmCriteria] Column:$column; Alias:$alias; Aggregate:$aggregate",ArrowLogger::EL_INFO);	
			if (!empty($alias) ){
        		$this->data['columns'][$alias] = array( 'column'=>$column, 'alias'=>$alias, 'aggregate'=>$aggregate);
        		$this->removeColumn($column);
			}
			else
        		$this->data['columns'][$column] = array( 'column'=>$column, 'alias'=>$column, 'aggregate'=>$aggregate);			
			if ($aggregate != ""){
				$this->aggregates = true;
			}
		}		
	}		
	
	/**
	 * Remove single column from criteria column list.
	 * @note Several colums separated by commas (,) can be suppled to this function.
	 * @param $columns
	 * @return 
	 */
	public function removeColumn($columns){
		//ArrowLogger::log("[ArrowOrmCriteria] Column(s) removed from selection; $columns",ArrowLogger::EL_INFO);	
		$data = explode(',',$columns);
		foreach ($data as $column){
			$column = trim($column);
			if ( !empty($column) && isset($this->data['columns'][$column])) 
			unset($this->data['columns'][$column]);
		}      
	}	
	
	/**
	 * 
	 * @param $column
	 * @return unknown_type
	 */
	public function addGroupBy($column){
		//ArrowLogger::log("[ArrowOrmCriteria] Group by \"{$column}\" added to selection; ",ArrowLogger::EL_INFO);	
		$this->data['group'][] =  $column;		
	}
	//---------------------------------------------------------------------------------------------------------	
	public function addOrderBy($column, $orderType=self::O_ASC, $order_priority ='' ){
		$orderType= strtoupper($orderType);
		//ArrowLogger::log("[ArrowOrmCriteria] Order by \"{$column} {$orderType}\" added to selection; ",ArrowLogger::EL_INFO);
		/*if ($orderType != self::O_ASC && $orderType != self::O_DESC && $orderType != self::O_RAND)
	      $orderType = self::O_ASC;*/
		$this->data['order'][] = array( $column, $orderType, $order_priority);
	}
	
	public function removeOrder(){
		$this->data['order'] = array();
	}
	
	public function removeGroup() {
	 	$this->data['group'] = array();
	}
	
	//---------------------------------------------------------------------------------------------------------
	public function setLimit( $offset, $lenght )
	{
		//ArrowLogger::log("[ArrowOrmCriteria] Limit set to: \"{$offset} {$lenght}\" ",ArrowLogger::EL_INFO);			
		$this->data['limit'] = array( $offset, $lenght );		
	}
	public function clearLimit()
	{
		//ArrowLogger::log("[ArrowOrmCriteria] Limit set to: \"{$offset} {$lenght}\" ",ArrowLogger::EL_INFO);			
		unset($this->data['limit']); 		
	}
	
	
	//---------------------------------------------------------------------------------------------------------	
	public function setDistinct( ){	
		//ArrowLogger::log("[ArrowOrmCriteria] Distinct set; ",ArrowLogger::EL_INFO);			
		$this->data['distinct']="DISTINCT";		
	}			
	//---------------------------------------------------------------------------------------------------------
	public function startGroup($groupType){
		$this->addCondition("",$groupType,self::START);
		//$this->groups[] = $groupType;
		//$this->firstlast = true;
		// przeniesione do addCondition
		//ArrowLogger::log("[Criteria] Nested {$groupType} group started",ArrowLogger::EL_INFO);					
	}
	//---------------------------------------------------------------------------------------------------------
	public function endGroup(){	
		//$this->groups = array_slice($this->groups,0,count($this->groups)-1);
		//$this->firstlast = true;           
		// przeniesione do addCondition
		$this->addCondition("",NULL,Criteria::END);
		//ArrowLogger::log("[Criteria] Nested group finished",ArrowLogger::EL_INFO);
	}	 
	//---------------------------------------------------------------------------------------------------------
  public function setModel($model, $force = false ){
		//ArrowLogger::log("[Criteria] Model set to: $model",ArrowLogger::EL_INFO);
    if ( (isset($this->data['model']) && $this->data['model'] != $model ) && !$force ) 
           throw new ArrowOrmException("[Criteria] Trying to set model $model to criteria where nodel has been set already to {$this->data['model']}", ArrowLogger::EL_ERROR);  
    $this->data['model'] = $model;             
	}
	//---------------------------------------------------------------------------------------------------------	
  public function setEmptyList(){
  	$this->data['columns'] = array();
    return $this->emptylist = false;
  }		
	//---------------------------------------------------------------------------------------------------------	
	public function fromString($string){
    $data = explode(';',$string);
    foreach ( $data as $obj ){
      $obj = explode(':',$obj, 3);
      if (count($obj) == 1)
        $this->add($obj[0]);
      elseif (count($obj) == 2)  
        $this->add($obj[0],$obj[1]);      
      elseif (count($obj) == 3)
        $this->add($obj[0],$obj[1],$obj[2]);
      else 
        throw new ArrowOrmException("[Criteria] Incorrect number of arguments; Condition ignored: ".implode(':',$obj), ArrowLogger::EL_WARNING);        
    } 
  }  
	//---------------------------------------------------------------------------------------------------------	
	public function toString(){
	$str = array();	
		$data = $this->data;
	//set special fields
	if ( isset ($data['model']) )
		$str[] = "SET:MODEL:{$data['model']}";
	if ( isset ($data['distinct']) )
		$str[] = "SET:DISTINCT";
	if ($this->emptylist == false)
		$str[] = "SET:EMPTYLIST";
	//columns

	if(isset($data['columns']))
		foreach ($data['columns'] as $col){
			if (!empty($col['aggregate']))
				$str[] = "{$col['column']}:{$col['alias']}:{$col['aggregate']}";
			elseif (!empty($col['alias']))
				$str[] = "{$col['column']}:{$col['alias']}";
			else
				$str[] = "{$col['column']}";
		}
		
	//$this->data['columns'][$alias] = array( 'column'=>$column, 'alias'=>$alias, 'aggregate'=>$aggregate);

	//conditions
	if(isset($data['conditions']))
		foreach ($data['conditions'] as $condition){
			$operator = $condition['condition'];
			$value = $condition['value'];	
			if (is_array($value))
			$value = implode(',',$value); 
			if (in_array($operator, self::$condition_names ) || $operator == '=') {
				if($operator == '=')
					$str[] = "{$condition['column']}:==:{$condition['value']}";
				else
					$str[] = "{$condition['column']}:{$condition['condition']}:{$value}";
			}
			elseif ($operator == self::START)
				$str[] = "GROUP:START:{$value}";
			elseif ($operator == self::END)
				$str[] = "GROUP:END";
		}		
	//order by
	if(isset($data['order']))
		foreach ($data['order'] as $order){
			$str[] = "ORDERBY:{$order[0]}:{$order[1]}";
		}	
	//group by
	if(isset($data['group']))
		foreach ($data['group'] as $group)
			$str[] = "GROUPBY:{$group}";
	// limit
	if (isset($data['limit']))
		$str[] = "LIMIT:{$limit[0]}:{$limit[1]}";
	
	return implode(';',$str);
  }   
	//---------------------------------------------------------------------------------------------------------	    
  public function getColumns(){
    $dbName = '_default_';
    $class = explode('.',$this->data['model']);
    $class = end($class);
    $conf = ArrowOrmConfig::getClassConf($class);
    $dbName = $conf['attr']['db'];
    $db = ArrowOrmSqlRouter::getDB($dbName);
    $sql = $db->columnsToSQL($this);
    return $sql;
  }
	//---------------------------------------------------------------------------------------------------------
  public function getConditions(){
    $dbName = '_default_';
    $class = explode('.',$this->data['model']);
    $class = end($class);
    $conf = ArrowOrmConfig::getClassConf($class);
    $dbName = $conf['attr']['db'];
    $db = ArrowOrmSqlRouter::getDB($dbName);
    $sql = $db->conditionsToSQL($this);
    return $sql;    
  }
	//---------------------------------------------------------------------------------------------------------
  public function getGroups(){
    $dbName = '_default_';
    $class = explode('.',$this->data['model']);
    $class = end($class);
    $conf = ArrowOrmConfig::getClassConf($class);
    $dbName = $conf['attr']['db'];
    $db = ArrowOrmSqlRouter::getDB($dbName);
    $sql = $db->groupsToSQL($this);
    return $sql;    
  }
  //---------------------------------------------------------------------------------------------------------
  public function getModel(){
  	return $this->data['model'];
  }
  //---------------------------------------------------------------------------------------------------------
  public function getData(){
  	try{
		if ($this->aggregates == false ){
			$model = $this->data['model'];
	    	$class = explode('.',$this->data['model']);
	    	$class = end($class);
			if ( $this->emptylist === true){
				$field_list = ArrowOrmConfig::getFieldList($class);
	      		foreach ($field_list as $field){
	        		if (!isset($this->data['columns'][$field]))
	          			$this->addColumn($field);
	      		}	
	       		$this->emptylist = false;
			}
			else{
				$key = ArrowOrmConfig::getKeyName($class);
				if (!isset($this->data['columns'][$key])){
					$this->addColumn($key);
				}
				//dadaj wymagane pola
				$field_list = ArrowOrmConfig::getRequiredFields($class);
	      		foreach ($field_list as $field){
	        		if ( !isset($this->data['columns'][$field]))
	          			$this->addColumn($field);
	      		}
			}	
	    }
  	}catch( Exception $ex ){
  		return $this->data;
  	}
    return $this->data;    
  } 
  
  public function isAggregated(){
    return $this->aggregates;
  }
  
 //---------------------------------------------------------------------------------------------------------------
 /*removes condition(s) from criteria 
  * set $value or $condiiton_type to null to ignore it 
  * 
  */  
 public function removeCondition($column,$value=null,$condition_type=null){
 	if (isset($this->data['conditions']))
	 	$conditions = $this->data['conditions'];
 	else
 		$conditions =array();
 	
 	$toRemove = array(); 
 	foreach ($conditions as $key=>$condition){
 		if ($condition['column'] == $column && ($value == null || $condition['value'] == $value) && ($condition_type == null || $condition['condition'] == $condition_type) ){
 			$prev = null;
 			$next = null;
 			$cond = $conditions[$key];
 			if (isset($conditions[$key-1]))
 				$prev = $conditions[$key-1];
 			if (isset($conditions[$key+1]))
 				$next = $conditions[$key+1];
 			$toRemove[]=$key;	
 			if ($next['condition'] == self::C_OR_GROUP || $next['condition'] == self::C_AND_GROUP ){
 				$toRemove[] = $key+1;
 			}
 			elseif ($prev['condition'] == self::C_OR_GROUP || $prev['condition'] == self::C_AND_GROUP ){
 				$toRemove[] = $key-1;
 			}
 		}
 	}
 	$newCond = array();
 	foreach ($conditions as $key=>$condition){
 		if (!in_array($key,$toRemove)){
 			$newCond[] = $condition;
 		}
 	}
 	$this->data['conditions'] = $newCond;
 }

/*Tries to replace given condition with new one
* Has two argument both being arrays
$old, $new
array('column', 'value','condition);
*/
 public function replaceCondition($old,$new){
 	$old_column = null;
 	$old_value = null;
 	$old_condition = null;
 	if (isset($old['column'])) $old_column = $old['column'];
 	if (isset($old['value'])) $old_value = $old['value'];
 	if (isset($old['condition'])) $old_condition = $old['condition']; 	
 	foreach ($this->data['conditions'] as $key=>&$condition){
 	
 		if ($condition['column'] == $old_column && ($old_value == null || $condition['value'] == $old_value) && ($old_condition == null || $condition['condition'] == $old_condition) ){
      if (isset($new['column'])) $condition['column'] = $new['column'];
      if (isset($new['value'])) $condition['value'] = $new['value'];
      if (isset($new['condition'])) $condition['condition'] = $new['condition'];
 		}
 	}
 }

 /*checks whether condition exists in criteria 
  * set $value or $condiiton_type to null to ignore it
  * 
  */  
 public function conditionExists($column,$value=null,$condition_type=null){
 	if (isset($this->data['conditions']))
	 	$conditions = $this->data['conditions'];
 	else
 		$conditions =array();
	foreach ($conditions as $key=>$condition){
 		if ($condition['column'] == $column && ($value == null || $condition['value'] == $value) && ($condition_type == null || $condition['condition'] == $condition_type) ){
 			return true;
 		}
 	}
 	return false;
 }
 
 public function isGroupBy() {
 	if( isset( $this->data['group'] ) )
 	return ( count( $this->data['group'] ) > 0 )? true : false ;
 	else return false ;
 }
 
}
?>